package scene;


// StereoCanvasGL.java
// Andrew Davison, June 2007, ad@fivedots.coe.psu.ac.th

/* A stereo scene is displayed, consisting of
   side-by-side views suitable for the left and right eyes. The
   3D effect is achieved by the user staring at the images using
   'parallel' eyes.

   The views can be switched, so they can be looked at by
   the user crossing their eyes.

   The scene contains:
     * a checkboard floor created by the Floor class

     * OBJ models (a rose in a vase, a penguin, and a couch)
       loaded using the OBJLoader package

   Key-based navigation is handled by a KeyStereoCamera instance,
   which also manages the two EyeCameras.

   The user can quit the game by pressing 'q', ctrl-c, the 'esc' key,
   or by clicking the close box

   StereoCanvasGL spawns a single thread which initialises the
   rendering and then loops, carrying out update, render, sleep with
   a fixed period.
*/

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import com.sun.opengl.util.*;
import com.sun.opengl.util.j2d.*;

//import OBJLoaderGL.*;   // my OBJ file loader package


public class StereoCanvasGL extends Canvas implements Runnable
{
  private static final int NO_DELAYS_PER_YIELD = 16;
  /* Number of renders with a sleep delay of 0 ms before the
     animation thread yields to other running threads. */

  private static final int MAX_RENDER_SKIPS = 5;   // was 2;
    // no. of renders that can be skipped in any one animation loop
    // i.e the games state is updated but not rendered

  private static final int NUM_FPS = 10;
    // number of FPS values stored to get an average

  private static final int CIRCLE_RADIUS = 10;
              // for the alignment circles used in the overlay

  private static final Color TRANSPARENT_BLACK =
                                   new Color(0.0f, 0.0f, 0.0f, 0.0f);

  private static final float LIGHT_DIR[] =  {1.0f, 1.0f, 1.0f, 0.0f};
                                 // right, top, front directional light


  // animation loop
  private long period;             // period between drawing in _nanosecs_
  private Thread animator;         // the thread that performs the animation
  private volatile boolean isRunning = false;   // used to stop the animation thread
  private volatile boolean isPaused = false;    // for pausing

  // OpenGL
  private GLDrawable drawable;  // the rendering 'surface'
  private GLContext context;    // the rendering context (holds rendering state info)
  private GL gl;
  private GLU glu;
  private GLUT glut;

  // window sizing
  private boolean isResized = false;
  private int panelWidth, panelHeight;

  // the stereo camera (consisting of left and right eye cameras)
  private KeyStereoCamera keyCamera;
  private boolean isCrossed;

//  private Floor floor;

  // the OBJ models
//  private OBJModel couchModel, penguinModel, roseVaseModel;

  // overlay
  private Overlay infoOverlay;
  private Font font;




  public StereoCanvasGL(long period, int width, int height,
                        GraphicsConfiguration config, GLCapabilities caps,
                        boolean isCrossed)
  { super(config);

    this.period = period;
    panelWidth = width;
    panelHeight = height;
    this.isCrossed = isCrossed;

    // setBackground(Color.white);

    // get a rendering surface and a context for this canvas
    drawable = GLDrawableFactory.getFactory().getGLDrawable(this, caps, null);
    context = drawable.createContext(null);

    keyCamera = null;
	addKeyListener( new KeyAdapter() {
       public void keyPressed(KeyEvent e)
       { processKey(e); }
    });

    infoOverlay = new Overlay(drawable);
    font = new Font("SansSerif", Font.BOLD, 14);
  } // end of StereoCanvasGL()


  private void processKey(KeyEvent e)
  /* Termination is dealt with here, but the navigation keys
     are passed to the KeyStereoCamera instance. */
  {
    int keyCode = e.getKeyCode();

    /* termination keys:
	      respond to esc, q, end, ctrl-c on the canvas */
    if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) ||
        (keyCode == KeyEvent.VK_END) ||
        ((keyCode == KeyEvent.VK_C) && e.isControlDown()) )
      isRunning = false;
    else {  // navigation keys
      if (keyCamera == null)
        System.out.println("Ignoring key press");
      else
        keyCamera.processKey(e);
    }
  }  // end of processKey()



  public void addNotify()
  // wait for the canvas to be added to the JPanel before starting
  {
    super.addNotify();      // creates the peer
    drawable.setRealized(true);  // the canvas can now be rendering into

    // initialise and start the animation thread
    if (animator == null || !isRunning) {
      animator = new Thread(this);
	  animator.start();
    }
  } // end of addNotify()


  // ------------- game life cycle methods ------------
  // called by the JFrame's window listener methods

  public void resumeGame()
  // called when the JFrame is activated / deiconified
  {  isPaused = false;  }

  public void pauseGame()
  // called when the JFrame is deactivated / iconified
  { isPaused = true;   }

  public void stopGame()
  // called when the JFrame is closing
  {  isRunning = false;   }

  // ----------------------------------------------

  public void reshape(int w, int h)
  /* called by the JFrame's ComponentListener when the window is resized
     (similar to the reshape() callback in GLEventListener) */
  {
     isResized = true;
     if (h==0)
       h = 1;  // to avoid division by 0 in the aspect ratio calculation
     panelWidth = w; panelHeight = h;
  }  // end of reshape()


  public void update(Graphics g) { }


  public void run()
  // initialize rendering and start frame generation
  {
    initRender();
    renderLoop();

    // discard the rendering context and exit
    context.destroy();
    System.exit(0);
  } // end of run()


  private void initRender()
  /* rendering initialization (similar to the init() callback
     in GLEventListener) */
  {
    makeContentCurrent();

    gl = context.getGL();
    glu = new GLU();
    glut = new GLUT();

    keyCamera = new KeyStereoCamera(gl, glu, isCrossed);

    // Update the viewport and frustum dimensions in the eye cameras
    keyCamera.updateViewVals(panelWidth, panelHeight);

    // various background colours
    // gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);     // black
    gl.glClearColor(0.17f, 0.65f, 0.92f, 1.0f);  // sky blue

    // z- (depth) selectBuffer initialization for hidden surface removal
    gl.glEnable(GL.GL_DEPTH_TEST);

    gl.glShadeModel(GL.GL_SMOOTH);    // use smooth shading

    addLight();

//    floor = new Floor(gl, glut);

    // load the OBJ models
//    couchModel = new OBJModel("couch", 2.0f, gl, false);
//    penguinModel = new OBJModel("penguin", gl);
//    roseVaseModel = new OBJModel("rose+vase", 3.2f, gl, false);

    /* release the context, otherwise the AWT lock on X11
       will not be released */
    context.release();
  }  // end of initRender()


  private void makeContentCurrent()
  // make the rendering context current for this thread
  {
    try {
      while (context.makeCurrent() == GLContext.CONTEXT_NOT_CURRENT) {
        System.out.println("Context not yet current...");
        Thread.sleep(100);
      }
    }
    catch (InterruptedException e)
    { e.printStackTrace(); }
  }  // end of makeContentCurrent()


  private void addLight()
  /* Set up a point source with ambient, diffuse, and specular
     colour components. The direction light is positioned at
     render time in renderEyeScene(). */
  {
    // enable a single light source
    gl.glEnable(GL.GL_LIGHTING);
    gl.glEnable(GL.GL_LIGHT0);

    float[] grayLight = {0.1f, 0.1f, 0.1f, 1.0f};  // weak gray ambient
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, grayLight, 0);

    float[] whiteLight = {1.0f, 1.0f, 1.0f, 1.0f};  // bright white diffuse & specular
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, whiteLight, 0);
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, whiteLight, 0);
  }  // end of addLight()


  // ---------------- frame-based rendering -----------------------


  private void renderLoop()
  /* Repeatedly update, render, and sleep, keeping to a fixed
     period as closely as possible. gather and report statistics.
  */
  {
    // timing-related variables
    long beforeTime, afterTime, timeDiff, sleepTime;
    long overSleepTime = 0L;
    int noDelays = 0;
    long excess = 0L;

    beforeTime = System.nanoTime();

    isRunning = true;

    while(isRunning) {
      makeContentCurrent();
      gameUpdate();

      renderScene();    // rendering
      drawable.swapBuffers();

      afterTime = System.nanoTime();
      timeDiff = afterTime - beforeTime;
      sleepTime = (period - timeDiff) - overSleepTime;

      if (sleepTime > 0) {   // some time left in this cycle
        try {
          Thread.sleep(sleepTime/1000000L);  // nano -> ms
        }
        catch(InterruptedException ex){}
        overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
      }
      else {    // sleepTime <= 0; this cycle took longer than the period
        excess -= sleepTime;  // store excess time value
        overSleepTime = 0L;

        if (++noDelays >= NO_DELAYS_PER_YIELD) {
          Thread.yield();   // give another thread a chance to run
          noDelays = 0;
        }
      }

      beforeTime = System.nanoTime();    // J3DTimer.getValue();

      /* If the rendering is taking too long, update the game state
         without rendering it, to get the updates/sec nearer to
         the required FPS. */
      int skips = 0;
      while((excess > period) && (skips < MAX_RENDER_SKIPS)) {
        excess -= period;
	    gameUpdate();    // update state but don't render
        skips++;
      }

      /* release the context, otherwise the AWT lock on X11
         will not be released */
      context.release();
	}
  } // end of renderLoop()


  private void gameUpdate()
  { if (!isPaused) {
      // do nothing;
    }
  }  // end of gameUpdate()



  // ------------------ rendering methods -----------------------------


  private void renderScene()
  {
    if (context.getCurrent() == null) {
      System.out.println("Current context is null");
      System.exit(0);
    }

    if (isResized) {
      // update the viewport and frustum dimensions in the eye cameras
      keyCamera.updateViewVals(panelWidth, panelHeight);
      isResized = false;
    }

    // clear colour and depth buffers
    gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

    renderEyeScene(EyeCamera.LEFT_EYE);  // left eye view of scene
    renderEyeScene(EyeCamera.RIGHT_EYE); // right eye

    showOverlayInfo();
  }  // end of renderScene()


  private void renderEyeScene(int eyeType)
  // draw the scene for the given eye camera
  {
    keyCamera.set(eyeType);
        // set an eye camera's viewport, frustum, and position

    // set light direction
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, LIGHT_DIR, 0);

    // draw the scenery
    gl.glPushMatrix();
//      floor.draw();
//      drawModels();
    gl.glPopMatrix();
  }  // end of renderEyeScene()






  private void showOverlayInfo()
  /* The overlay contains a vertical line dividing the two
     camera views, three circles across the middle to help
     the user align their eyes, and camera position details
     at the top left.
     The code utilizes the JOGL Overlay utility class.
  */
  {
    // draw over the entire window
    gl.glViewport(0, 0, panelWidth, panelHeight);
    gl.glMatrixMode(GL.GL_PROJECTION);
    gl.glLoadIdentity();

    Graphics2D g2d = infoOverlay.createGraphics();

    // clear the overlay
    g2d.setComposite(AlphaComposite.Src);
    g2d.setColor(TRANSPARENT_BLACK);
    g2d.fillRect(0, 0, panelWidth, panelHeight);

    g2d.setColor(Color.YELLOW);
    g2d.drawLine(panelWidth/2, 0, panelWidth/2, panelHeight);   // draw a vertical line

    // draw three eye alignment circles
    int len = CIRCLE_RADIUS*2;
    int yPosn = panelHeight/2-CIRCLE_RADIUS;

    g2d.fillOval(-CIRCLE_RADIUS, yPosn, len, len);               // middle left edge
    g2d.fillOval(panelWidth/2-CIRCLE_RADIUS, yPosn, len, len);   // middle of window
    g2d.fillOval(panelWidth-CIRCLE_RADIUS, yPosn, len, len);     // middle right edge

    // draw camera info (in white)
    g2d.setColor(Color.WHITE);
    g2d.setFont(font);
    g2d.drawString( keyCamera.getPos(), 5, 15);
    g2d.drawString( keyCamera.getRots(), 5, 33);

    // render all the overlay to the screen
    infoOverlay.markDirty(0, 0, panelWidth, panelHeight);
    infoOverlay.drawAll();

    g2d.dispose();
  }  // end of showOverlayInfo()


} // end of StereoCanvasGL class

